package org.jordens.jdbcspy;

import org.jordens.jdbcspy.jmx.*;

import java.lang.reflect.*;
import java.sql.*;
import java.util.*;

/**
 * JDBC Connection implementation that wraps another JDBC connection and logs
 * Statement, PreparedStatement and CallableStatement executions.
 *
 * @author Adam Jordens
 */
public class JDBCSpyConnection implements Connection
{
    private static final JDBCSpy jdbcSpy;

    private final Connection delegate;

    static
    {
        jdbcSpy = new JDBCSpy();
        jdbcSpy.registerBean();
    }

    /**
     * @param wrappedConnection JDBC connection to wrap
     */
    public JDBCSpyConnection(Connection wrappedConnection)
    {
        this.delegate = wrappedConnection;
    }

    /**
     * {@inheritDoc}
     */
    public Statement createStatement() throws SQLException
    {
        return createProxy(Statement.class, new GenericStatementHandler(delegate.createStatement()));
    }

    /**
     * {@inheritDoc}
     */
    public PreparedStatement prepareStatement(String s) throws SQLException
    {
        return createProxy(PreparedStatement.class, new GenericStatementHandler(delegate.prepareStatement(s)));
    }

    /**
     * {@inheritDoc}
     */
    public CallableStatement prepareCall(String s) throws SQLException
    {
        return createProxy(CallableStatement.class, new GenericStatementHandler(delegate.prepareCall(s)));
    }

    /**
     * {@inheritDoc}
     */
    public Statement createStatement(int i, int i1) throws SQLException
    {
        return createProxy(Statement.class, new GenericStatementHandler(delegate.createStatement(i, i1)));
    }

    /**
     * {@inheritDoc}
     */
    public PreparedStatement prepareStatement(String s, int i, int i1) throws SQLException
    {
        return createProxy(PreparedStatement.class, new GenericStatementHandler(delegate.prepareStatement(s, i, i1)));
    }

    /**
     * {@inheritDoc}
     */
    public CallableStatement prepareCall(String s, int i, int i1) throws SQLException
    {
        return createProxy(CallableStatement.class, new GenericStatementHandler(delegate.prepareCall(s, i, i1)));
    }

    /**
     * {@inheritDoc}
     */
    public Statement createStatement(int i, int i1, int i2) throws SQLException
    {
        return createProxy(Statement.class, new GenericStatementHandler(delegate.createStatement(i, i1, i2)));
    }

    /**
     * {@inheritDoc}
     */
    public PreparedStatement prepareStatement(String s, int i, int i1, int i2) throws SQLException
    {
        return createProxy(PreparedStatement.class, new GenericStatementHandler(delegate.prepareStatement(s, i, i1, i2)));
    }

    /**
     * {@inheritDoc}
     */
    public CallableStatement prepareCall(String s, int i, int i1, int i2) throws SQLException
    {
        return createProxy(CallableStatement.class, new GenericStatementHandler(delegate.prepareCall(s, i, i1, i2)));
    }

    /**
     * {@inheritDoc}
     */
    public PreparedStatement prepareStatement(String s, int i) throws SQLException
    {
        return createProxy(PreparedStatement.class, new GenericStatementHandler(delegate.prepareStatement(s, i)));
    }

    /**
     * {@inheritDoc}
     */
    public PreparedStatement prepareStatement(String s, int[] ints) throws SQLException
    {
        return createProxy(PreparedStatement.class, new GenericStatementHandler(delegate.prepareStatement(s, ints)));
    }

    /**
     * {@inheritDoc}
     */
    public PreparedStatement prepareStatement(String s, String[] strings) throws SQLException
    {
        return createProxy(PreparedStatement.class, new GenericStatementHandler(delegate.prepareStatement(s, strings)));
    }

    /**
     * {@inheritDoc}
     */
    public String nativeSQL(String s) throws SQLException
    {
        return delegate.nativeSQL(s);
    }

    /**
     * {@inheritDoc}
     */
    public void setAutoCommit(boolean b) throws SQLException
    {
        delegate.setAutoCommit(b);
    }

    /**
     * {@inheritDoc}
     */
    public boolean getAutoCommit() throws SQLException
    {
        return delegate.getAutoCommit();
    }

    /**
     * {@inheritDoc}
     */
    public void commit() throws SQLException
    {
        delegate.commit();
    }

    /**
     * {@inheritDoc}
     */
    public void rollback() throws SQLException
    {
        delegate.rollback();
    }

    /**
     * {@inheritDoc}
     */
    public void close() throws SQLException
    {
        delegate.close();
    }

    /**
     * {@inheritDoc}
     */
    public boolean isClosed() throws SQLException
    {
        return delegate.isClosed();
    }

    /**
     * {@inheritDoc}
     */
    public DatabaseMetaData getMetaData() throws SQLException
    {
        return delegate.getMetaData();
    }

    /**
     * {@inheritDoc}
     */
    public void setReadOnly(boolean b) throws SQLException
    {
        delegate.setReadOnly(b);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isReadOnly() throws SQLException
    {
        return delegate.isReadOnly();
    }

    /**
     * {@inheritDoc}
     */
    public void setCatalog(String s) throws SQLException
    {
        delegate.setCatalog(s);
    }

    /**
     * {@inheritDoc}
     */
    public String getCatalog() throws SQLException
    {
        return delegate.getCatalog();
    }

    /**
     * {@inheritDoc}
     */
    public void setTransactionIsolation(int i) throws SQLException
    {
        delegate.setTransactionIsolation(i);
    }

    /**
     * {@inheritDoc}
     */
    public int getTransactionIsolation() throws SQLException
    {
        return delegate.getTransactionIsolation();
    }

    /**
     * {@inheritDoc}
     */
    public SQLWarning getWarnings() throws SQLException
    {
        return delegate.getWarnings();
    }

    /**
     * {@inheritDoc}
     */
    public void clearWarnings() throws SQLException
    {
        delegate.clearWarnings();
    }

    /**
     * {@inheritDoc}
     */
    public Map<String, Class<?>> getTypeMap() throws SQLException
    {
        return delegate.getTypeMap();
    }

    /**
     * {@inheritDoc}
     */
    public void setTypeMap(Map<String, Class<?>> map) throws SQLException
    {
        delegate.setTypeMap(map);
    }

    /**
     * {@inheritDoc}
     */
    public void setHoldability(int i) throws SQLException
    {
        delegate.setHoldability(i);
    }

    /**
     * {@inheritDoc}
     */
    public int getHoldability() throws SQLException
    {
        return delegate.getHoldability();
    }

    /**
     * {@inheritDoc}
     */
    public Savepoint setSavepoint() throws SQLException
    {
        return delegate.setSavepoint();
    }

    /**
     * {@inheritDoc}
     */
    public Savepoint setSavepoint(String s) throws SQLException
    {
        return delegate.setSavepoint(s);
    }

    /**
     * {@inheritDoc}
     */
    public void rollback(Savepoint savepoint) throws SQLException
    {
        delegate.rollback(savepoint);
    }

    /**
     * {@inheritDoc}
     */
    public void releaseSavepoint(Savepoint savepoint) throws SQLException
    {
        delegate.releaseSavepoint(savepoint);
    }

    /**
     * @param clazz Statement class to proxy
     * @param handler Invocation handler 
     * @return Dynamic proxy of a JDBC Statement
     */
    <T extends Statement> T createProxy(Class<T> clazz, InvocationHandler handler)
    {
        return (T)Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] {clazz}, handler);
    }

    /**
     * For testing purposes only!
     *
     * @return The local reference to JDBCSpy
     */
    JDBCSpy getJDBCSpy()
    {
        return jdbcSpy;
    }

    /**
     * A generic wrapper for JDBC Statements that will log query executions
     * and expose those statistics via the JDBCSpy MBean.
     */
    private class GenericStatementHandler implements InvocationHandler
    {
        private final Statement delegate;

        /**
         * @param statement JDBC Statement to wrap and delegate to
         */
        public GenericStatementHandler(Statement statement)
        {
            this.delegate = statement;
        }

        /**
         * {@inheritDoc}
         */
        public Object invoke(Object o, Method method, Object[] objects) throws Throwable
        {
            long start = System.currentTimeMillis();
            try
            {
                return method.invoke(delegate, objects);
            }
            finally
            {
                long end = System.currentTimeMillis();
                if (method.getName().equals("executeQuery"))
                {
                    if (objects == null)
                    {
                        jdbcSpy.logQuery(o.toString(), (end-start));
                    }
                    else
                    {
                        jdbcSpy.logQuery(Arrays.asList(objects).toString(), (end-start));
                    }
                }
            }
        }
    }
}
